content: an inline image that spells an entry twice must not be read twice - #13
content: an inline image that spells an entry twice must not be read twice#13tannevaled wants to merge 1 commit into
Conversation
…twice Expanded copies an inline image's dictionary, writing the abbreviations out: /BPC becomes /BitsPerComponent, /W becomes /Width. A dictionary may carry both spellings of the same entry, and one of them has to win. Which one depended on the order Go happened to walk the map in, and Go deliberately walks it in a different order every time. So the same file, read twice by the same program, gave two different answers. The expanded dictionary is also what says whether a candidate stretch of bytes really is the whole of the image — it carries the filters the data has to run through — so an inline image whose /F and /Filter named different filters moved the end of the image, and every operation after it belonged to a different stream. issue14256.pdf in mozilla's pdf.js corpus tokenised as 58 operations on some runs and 118 on others, with no error either time. Neither was right: it holds 119. The abbreviation wins now. It is the spelling the specification defines for an inline image, so a producer that wrote /BPC 8 meant eight, and the long form gives way. What matters more than the choice is that it is a choice, made the same way every time. Two passes rather than a sort: the long and unknown keys first, then the abbreviations written out on top of them. Found by hashing what 21 311 corpus files say and running it twice. Every other file gave the same answer both times; this one gave four different answers in five runs. This commit also adds the fuzz targets. There were none in this repository. FuzzOpen, FuzzParseObject, FuzzOperations and FuzzDecode ran for a combined forty-seven minutes and 73 million executions without finding a crasher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9ae5c76 to
85cef51
Compare
|
Superseded. The fix for the inline-image map order landed as #14, written independently from the same measurement and agreeing with this branch to the line — two passes over the dictionary, the written-out names first and the abbreviations over them, with the abbreviation winning. The useful half of this branch is not the fix but its fuzz targets, and those are now #17: ParseObject, Operations and Decode had none, and this branch's time budget is what makes them worth running. Seeded from mozilla's 1 437 adversarial PDFs, FuzzOpenWithinABudget runs 3 077 612 executions without a failure. One thing found while salvaging it: this branch's FuzzOpen has the same name as the one in recover_test.go. They compile, but |
Found by a robustness campaign: hashing what 21 311 corpus files say, then running it twice. Every other file gave the same answer both times.
The defect: reading the same file twice gave two different documents
InlineImage.Expandedcopies an inline image's dictionary, writing the abbreviations out —/BPCbecomes/BitsPerComponent,/Wbecomes/Width. A dictionary may carry both spellings of the same entry, and one of them has to win:issue14256.pdfin mozilla's pdf.js corpus is exactly that file — it is in their suite for this reason. Its inline images are written like:Why it is worse than a wrong pixel
The expanded dictionary is what
inlineImageEnduses to decide whether a candidate stretch of bytes really is the whole of the image — it carries the filters the data has to run through. Get/Filterwrong and the candidate fails to decode, so the scanner keeps looking and takes a differentEI. Every operation after that belongs to a different stream.Measured on
issue14256.pdf,reader.Operationsover identical bytes:Neither was right. With the fix it is 119 operations, 40 runs out of 40.
No error was reported on any run. This is the silent-wrongness category: the page renders differently on different runs of the same program on the same file, and nothing anywhere says so.
The fix
The abbreviation wins. It is the spelling the specification defines for an inline image, so a producer that wrote
/BPC 8meant eight; the long form is the tolerated alternative and gives way. What matters more than the choice is that it is a choice, made the same way every time.Two passes rather than a sort: the long and unknown keys first, then the abbreviations written out on top of them.
The regression test
TestInlineImageExpandedIsDeterministiccovers all ten colliding pairs (BPC/W/H/F/CS/D/I/L/IM/DP), 200 attempts each — Go randomises map order per walk, so one run of a two-key collision picks wrong about half the time and 200 make a survivor certain to show. Against the parent commit all ten subtests fail:TestOperationsIsAFunctionOfItsBytesis the end-to-end one: a content stream with an inline image naming/F [/AHx]and/Filter [/A85], tokenised 200 times. It fails 5 runs out of 5 against the parent commit and passes 5 out of 5 here.Corpus-wide check
The text and image metadata of 21 311 files — every page's runs with their positions, sizes, fonts and flags, the assembled text, and every image's dimensions, placement, filter and content hash — hashed before and after. One file differed, and it was this one, differing from itself.
Fuzz targets
There were none in this repository. This adds
FuzzOpen,FuzzParseObject,FuzzOperationsandFuzzDecode(which drives Flate/LZW/ASCII85/ASCIIHex/RunLength and the predictors, with the first byte picking the filter so one corpus explores all of them). Each enforces a per-input time budget, because the highest-value defect in this space is a small file that costs a large amount of time, andgo test -fuzz's own timeout kills the process without saying which input was at fault.They ran for a combined 47 minutes and 73 million executions without finding a crasher:
FuzzOpenFuzzDecodeFuzzParseObjectFuzzOperationsPoint
PDF_SEEDSat a corpus to seed from it; without it the built-in seeds and anything undertestdatastill run. (The first version skipped the whole target when the corpus was absent — which would have skipped the committed crashers too. Fixed.)What else was looked for and not found
Open, every page, every stream decoded, every font,extract,forms,opsround-trip andrender: no panics, no non-termination.reader.Gates
go test ./...green, 100.0% of statementsgo vetclean,gofmtclean,CGO_ENABLED=0🤖 Generated with Claude Code